''' FiledObjects are simply objects which may be efficiently stored to disk,
    and easily incorporated into other FiledObjects.
    Also some classes which are used by Map and Menu, but which are too trivial to be FiledObjects themselves.
'''
try: import cPickle as pickle
except ImportError: import pickle

# refactored from http://en.wikipedia.org/wiki/LZW

def encrypt(obj):
    ''' take a python object, and encrypt it into a string
        [which may be written to a file or socket].
    '''
    chars = 256
    mydict = {}
    buffer = []
    result = ''
    for i in xrange(chars):
        mydict[str(i)] = i
    for i in pickle.dumps(obj):
        if len(buffer) == 0:
            xstr = str(ord(i))
        else:
            xstr = '-'.join([str(elem) for elem in (buffer + [ord(i)])])
        if mydict.has_key(xstr):
            buffer.append(ord(i))
        else:
            result += unichr(mydict['-'.join([str(elem) for elem in buffer])]).encode('utf8')
            mydict[xstr] = chars
            chars += 1
            buffer = []
            buffer.append(ord(i))
    if len(buffer) != 0:
        result += unichr(mydict['-'.join([str(elem) for elem in buffer])]).encode('utf8')
    return result

def decrypt(raw):
    ''' take a string [which may have come from a file or socket or zipfile],
        and decrypt it back into a python object.
    '''
    chars = 256
    mydict = {}
    for i in xrange(chars):
        mydict[i] = unichr(i).encode('utf8')
    buffer = ''
    chain = ''
    result = ''
    for i in raw.decode('string_escape').decode('utf8'):
        code = ord(i)
        if buffer:
            if code <= 255:
                current = mydict[code]
                result += current
                chain = buffer+current
                mydict[chars] = chain
                chars += 1
                buffer = current
            else:
                if mydict.has_key(code):
                    chain = mydict[code]
                else:
                    chain = buffer + buffer[0]
                result += chain
                mydict[chars] = buffer + chain[0]
                chars += 1
                buffer = chain
        else:
            current = mydict[code]
            buffer = current
            result += current
    return pickle.loads(result)
    
class FiledObject(object):
    ''' a pickle-able superclass which allows this object to be
        concisely embedded in another FiledObject pickle file
        by merely pointing to a private pickle file which is
        written by the save method.
    '''
    AUTOSAVE = True	# whether to call save in __setitem__
    
    def __init__(self, fn):
        ''' load information from the encryped pickle file fn.
        '''
        self.__fn__ = fn
        self.__dict__.update(decrypt(self._find(fn)))
    
    __setstate__ = __init__
    
    def __getstate__(self):
        ''' tell pickle that all it needs to know about me is self.__fn__.
        '''
        return self.__fn__
    
    def_abstract_method('_find', 'convert a simple filename "foo" to an absolute filename "/.../pykemon-1.0/default-mapset/foo"')
    
    def __setattr__(self, name, value):
        ''' hook to optionally automatically call save.
        '''
        self.__dict__[name] = value
        if self.AUTOSAVE: self.save()
    
    def save(self):
        ''' dump my information to an encrypted pickle file self.__fn__.
        '''
        file(self.__fn__, 'w').write(encrypt(
            dict([
                (k, v)
                for k, v in self.__dict__.iteritems()
                if k != '__fn__'
            ])
        )
    
    def __repr__(self):
        return '%s(%r)' % (self.__class__.__name__, self.__fn__)
    
    __str__ = __repr__
    

class Map(FiledObject):
    ''' when decrypted, the pickle file should be a dict similar to {
            'name': 'Foo Town',
            'tiles': [[Tile('dirt'), ...], [Tile('dirt'), ...], ...],
            'chars': [((10, 10), Char('Fred')), ((20, 20), Char('George')), ...],
            'items': [((30, 30), Item('meat')), ((40, 40), Item('candy')), ...],
            'music': Music('foo'),
            'd_encounter': (13.2, 3.7),	# the number of steps until you next encounter a wild pykemon is round(random.normalvariate(13.2, 3.7))
            'wild': [	# wild pykemon you may encounter in this map
                (PykemonSpecies('quux'), 0.1, (10.8, 2.1), [
                    # 0.1 == on average, every ten encounters in any of the following rects, you find a wild quux pykemon
                    # (10.8, 2.1) == the level of that wild quux will be round(random.normalvariate(10.8, 2.1))
                    pygame.Rect((10, 10, 30, 30)),
                    pygame.Rect((40, 10, 30, 30)),
                    ...
                ]),
                (PykemonSpecies('quuz'), 0.2, (20.2, 3.4), [pygame.Rect((10, 10, 30, 30))]),
                ...
            ],
            '_enter': '',	# python code run when player enters map
            '_exit': '',	# "" exits map
        }
    '''
    _find = staticmethod(lambda fn: data_path('maps', config.MAPSET, fn))
    
    def __getitem__(self, (x, y)):
        ''' returns the Tile object at that coordinate.
            self[10, 10] == Tile(...)
        '''
        tile = self.tiles[x][y]
        if not isinstance(tile, Tile):
            tile = Tile('_blank')
            self[x, y] = tile
        return tile
    
    def __setitem__(self, (x, y), tile):
        while len(self.tiles) <= x: self.tiles.append([])
        while len(self.tiles[x]) <= y: self.tiles[x].append(None)
        self.tiles[x][y] = tile
    

class Tile(object):
    ''' an image and an optional link to another map.
        not complex enough to need to be a FiledObject.
    '''
    def __init__(self, img_fn):
        ''' img_fn is the image drawn by a Strolling Mode,
            to_map is None or a Map to which user goes when they enter this tile,
            to_map_coord is the coord on the to_map corresponding to this tile.
        '''
        self.img_fn = self._find(img_fn)
        self.image, self.rect = load_image(self.img_fn)
    
    _find = staticmethod(lambda fn: data_path('tiles', config.TILESET, fn))
    
    __setstate__ = __init__
    
    def __getstate__(self):
        return (self.img_fn, self.to_map, self.to_map_coord)
    
    def __repr__(self):
        return '%s(%r)' % (self.__class__.__name__, self.fn)
    
    __str__ = __repr__
    
class DoorTile(Tile):
    def __init__(self, img_fn, to_map, to_map_coord, to_map_direction=None):
        ''' img_fn is the image drawn by a Strolling Mode,
            to_map is None or a Map to which user goes when they enter this tile,
            to_map_coord is the coord on the to_map corresponding to this tile.
        '''
        Tile.__init__(self, img_fn)
        self.to_map = to_map
        self.to_map_coord = to_map_coord
    
    __setstate__ = __init__
    
    def __getstate__(self):
        return (self.img_fn, self.to_map, self.to_map_coord, self.to_map_direction)
    
    def __repr__(self):
        return '%s(%r, %r, %r, %r)' % (self.__class__.__name__, self.img_fn, self.to_map, self.to_map_coord, self.to_map_direction)
    
    __str__ = __repr__

class CodeTile(Tile):
    ''' useful for eg. locked doors, PCs, minigames, and anything that you can walk up to and hit enter to "use".
    '''
    def __init__(self, img_fn, code):
        Tile.__init__(self, img_fn)
        self.code = code
    
    __setstate__ = __init__
    
    def __getstate__(self):
        return (self.img_fn, self.to_map, self.to_map_coord, to_map_direction)
    
    def __repr__(self):
        return '%s(%r, %r)' % (self.__class__.__name__, self.img_fn, self.code)
    
    def __call__(self):
        exec self.code
    
    __str__ = __repr__

class WhenLevelUp(object):
    ''' contains a python expression "cond" which, if evaluates to True, indicates that a Pykemon is ready to evolve.
    '''
    def __init__(self, cond='True'): self.cond = cond

class WhenTraded(WhenLevelUp):
    ''' same as a WhenLevelUp, but evaluated when the Pykemon is traded.
    '''

class WhenGiven(object):
    ''' like a WhenLevelUp, but evaluated when the Pykemon is given /this/ item.
    '''
    def __init__(self, item, cond='True'): self.item, self.cond = item, cond

class PykemonSpecies(FiledObject):
    ''' when decrypted, the pickle file should be a dict similar to {
            'img': 'quux-42.jpg',
            'desc': 'Cuddly.',
            'colors': [COLOR_COLORLESS, COLOR_LIGHT, COLOR_WHITE],
            'abilities': [
                (Ability('foo'), (12, 2)),	# the level at which any quux learns is round(random.normalvariate(12, 2))
                (Ability('bar'), (14, 3)),
            ],
            'evolves': [
                (PykemonSpecies('quuz'), (42, 8)),	# the level at which any quux evolves into a quuz is round(random.normalvariate(42, 8))
                (PykemonSpecies('quuw'), (57, 9)),
                (PykemonSpecies('quuu'), WhenTraded()),	# trading a quux will always evolve it into a quuu
                (PykemonSpecies('quuua'), WhenTraded('self.level > 32')),
                # giving a sufficiently happy quux a moon stone to hold will evolve it into a quut
                (PykemonSpecies('quut'), WhenGiven(Item('moon stone'), 'self.happiness > 50')),
                (PykemonSpecies('quugrin'), WhenLevelUp('self.happiness > 100')),
                # when a quux becomes sufficiently happy, it will evolve into a quugrin
                (PykemonSpecies('quufoo'), WhenLevelUp('self.holding == Item("foo rock")')),
            ],
            'hatch': (1e6, 1e2),	# the time it takes for an egg of this species to hatch is round(random.normalvariate(1e6,1e2))
            # if a quux evolves from a quua and cannot be hatched from an egg, the previous line would say "'hatch': PykemonSpecies('quua'),"
            # the following describe the rates [per level] at which the Pykemon's statistics change; again, round(random.normalvariate(...))
            'd_agility':	(4.2, 0.2),
            'd_attack':		(4.2, 0.2),
            'd_defense':	(4.2, 0.2),
            'd_exp_needed':	(42.0, 2.0),
            'd_total_hp':	(4.2, 0.2),
            'd_sp_attack':	(4.2, 0.2),
            'd_sp_defense':	(4.2, 0.2),
            'd_speed':		(4.2, 0.2),
            'd_strength':	(4.2, 0.2),
        }
    '''
    _find = staticmethod(lambda fn: data_path('species', fn))
    

class Pykemon(FiledObject):
    ''' represents a particular pykemon.
        when decrypted, the pickle file should be a dict similar to {
            'species': PykemonSpecies('quux'),
            'name': 'My Cuddly Quux',
            'level': 42,
            'sex': SEX_MALE,
            'hatch_time': 0,	# time till this pykemon hatches
            'cond': CONDITION_OK,
            'holding': Item('foo'),
            'hp': 420,
            'abilities': [Ability('foo'), Ability('bar')],
            'future_abilities': [(Ability('baz'), 45)],	# this quux will learn baz at level 45
            'pps': [42, 17],	# power points left for abilities foo and bar
            'evolves': [
                (PykemonSpecies('quuz'), 47),	# this quux will evolve into a quuz at level 47
            ],
            'exp': 42,
            'agility': 42.0,
            'attack': 42.0,
            'defense': 42.0,
            'exp_needed': 420.3,
            'happiness': 42.0,
            'total_hp': 420.3b,
            'sp_attack': 42.0,
            'sp_defense': 42.0
            'speed': 42.0,
            'strength': 42.0,
        }
    '''
    def _find(fn):
        p = data_path('pykemon')
        default = os.path.join(p, 'default')
        if fn in os.listdir(default): return os.path.join(default, fn)
        else:
            saved = os.path.join(p, MAIN.player.__fn__)
            if fn in os.listdir(saved): return os.path.join(saved, fn)
            else: raise ValueError('unable to find pykemon "%r"' % fn)
    _find = staticmethod(fn)
    
    def __init__(self, fn='', **kw):
        ''' if fn is specified, it is passed to FiledObject.__init__
            otherwise, all the necessary attributes must be specified in kw.
        '''
        if fn: FiledObject.__init__(self, fn)
        else: self.__dict__.update(**kw)
    
    def wild(cls, map, (x, y)):
        pos = pygame.Rect((x, y, 0, 0))
        species, (level_mu, level_stddev) = weighted_choice([
            ((species, level_mu_stddev), p)
            for species, p, level_mu_stddev, rects in map.wild
            if any([
                rect.contains(pos)
                for rect in rects
            ])
        ])
        return cls(**kw)
    wild = classmethod(wild)

class Item(FiledObject):
    ''' when decrypted, the pickle file should be a dict similar to {
            'code': """	# python code executed when the item is used
                MAIN.player.carrying_pykemon[0].__dict__.update({'cond': CONDITION_OK, 'hp':
            """
        }
    '''
    _find = staticmethod(lambda fn: data_path('items', fn))
    
    def __call__(self):
        exec self.code in XXX

class Player(FiledObject):
    ''' contains all the information that make Alyssa's game different from Bobby's game,
        such as what pykemon and items they have, which map and where on it they currently are, etc.
        
        when decrypted, the pickle file should be a dict similar to {
            'name': 'Bob',
            'carrying_pykemon': [Pykemon('fred'), Pykemon('george')],
            'carrying_items': [Item('foo'), Item('bar')],
            'stored_pykemon': [Pykemon('alice'), Pykemon('bob')],
            'stored_items': [Item('baz')],
            'current_location': (Map('city_town'), (10, 10)),
            'collected_pykemon': 1023,	# bit string: 1 indicates that a pykemon species has been collected
            'badges': 255,	# bit string: 1 indicates that a badge has been earned
            'colors_learned': {
                COLOR_WHITE: ({COLOR_BLACK: False}, {COLOR_GREY:True}, {}, {}, {}, {COLOR_ROCK:False}),
                ...
            },
            'maps_learned': {
                Map('foo_town'): True,
                Map('bar_town'): False,
                ...
            },
            'game_time': 1234567890,	# number of game-seconds played = number of real seconds / config.GAME_TIME_RATE
            'last_played': 1234567890,	# last epoch second that you played
        }
    '''
    _find = staticmethod(lambda fn: data_path('saved', fn))
    
    def _new(cls):
        ''' 
        '''
        
    _new = classmethod(_new)
    
    def count_badges(self):
        n = 0
        badges = self.badges
        while badges > 0:
            
    
    def count_collected(self):
        
    
    def summary(self):
        return '%-16s  %2d       %3d   %2d:%s:%s   %15s' % (
            self.name,
            self.count_badges(),
            self.count_collected(),
            (self.game_time / 3600),
            str((self.game_time / 60) % 60).rjust(2, '0'),
            str(self.game_time % 60).rjust(2, '0'),
            time.strftime('%d %b %X', self.localtime(self.last_played)),
        )

class Char(FiledObject):
    ''' represents an in-game character that you can talk to, perhaps receive/give items from/to, perhaps battle.
        
        when decrypted, the pickle file should be a dict similar to {
            'tile': Tile('fred'),
            'img_fn': 'Fred.jpg',	# image displayed in battle
            'dialog': """	# python code
                self.say('hello!')
                if self.ask('hi', 'go away') == 0:
                    self.say("you're friendly!")
                    self.give(Item('present'))
                else:
                    self.say('boo to you, too.')
                    self.battle()
            """,
            'carrying_pykemon': [Pykemon("fred's quux")],
        }
    '''
    def _find(self, fn): return data_path('chars', fn)
    
    def __call__(self):
        exec self.dialog
    
    def battle(self):
        ''' convenience method for self.dialog which engages user in battle with char
        '''
        XXX
    
    def say(self, s):
        ''' convenience method for self.dialog which displays s
        '''
        XXX
    
    def ask(self, *reply_options):
        ''' convenience method for self.dialog which allows user to choose between reply_options, then returns user's selection
        '''
        return XXX
    
    def give(self, item):
        ''' convenience method for self.dialog which gives item to user
        '''
        XXX

class Ability(FiledObject):
    ''' contains generic code describing, for example, what happens when you
        instruct your quux to Tackle its opponent or Cut down the bush in front of you.
        
        when decrypted, the pickle file should be a dict similar to {
            'code': """	# python code
                enemy.defense *= 0.9
            """,
            'strolling_code': '',	# empty string indicates that this ability is unavailable outside a battle
            'special': False,		# whether this ability is physical (True) or non-physical
            'pp': 20,	# power points: total number of times this ability may be used between hospital visits
        }
    '''
    def _find(self, fn): return data_path('abilities', fn)
    
    def __call__(self):
        exec self.code in XXX

class Menu(FiledObject):
    ''' contains all the text and code needed by the MainMenu, and some of the text and code for Battle and Char.dialog.
        when decrypted, the pickle file should be a dict similar to {
            'options': odict([
                (), 
            ]),
        }
    '''
    
    